package at.jku.pervasive;

import java.awt.Color;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.collections.Bag;
import org.apache.commons.collections.bag.HashBag;

import at.jku.pervasive.filter.Filter;
import at.jku.pervasive.filter.MeanFilter;
import at.jku.pervasive.gui.jformdesigner.ReplayMeasurementPanel;
import at.jku.pervasive.recognition.AngleGestureRecognizer;
import at.jku.pervasive.recognition.Gesture;
import at.jku.pervasive.recognition.GestureCharacteristics;
import at.jku.pervasive.recognition.GestureObject;
import at.jku.pervasive.recognition.GestureRecognizer;

public class Application implements PositionListener
{

	private static final Application INSTANCE = new Application();

	public static Application getInstance()
	{
		return INSTANCE;
	}

	public static final int MAX_NUMBER_OF_RECENT_TAGS = 3;

	private List<PositionListener> posListeners;
	private List<ApplicationListener> appListeners;

	private final Color[] availableColors;
	private int currentColorIndex;

	private DateFormat dateFormat;
	private String host;

	private UbisenseListener listener;

	private int port;
	private File saveFolder;

	private final List<String> tracedTagIds;

	private Map<TagPosition, String> tagAssignment = new TreeMap<TagPosition, String>();
	private Map<String, TagPosition> reverseTagAssignment = new TreeMap<String, TagPosition>();

	private Map<String, Color> tagIdColors = new TreeMap<String, Color>();

	private Map<TagPosition, LinkedList<Position>> recentTagPositions;

	private Application()
	{
		super();

		this.appListeners = new LinkedList<ApplicationListener>();
		this.posListeners = new LinkedList<PositionListener>();

		this.listener = new UbisenseListener();
		this.listener.addPositionListener(this);

		this.dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss");
		this.saveFolder = new File(System.getProperty("user.home") + "/Documents/JKU");

		this.tracedTagIds = new LinkedList<String>();

		this.availableColors = new Color[] { Color.BLUE, Color.RED, Color.GREEN, Color.MAGENTA, Color.YELLOW };

		// this.host = "192.168.93.10";
		this.host = "localhost";
		this.port = 6666;

		this.recentTagPositions = new TreeMap<TagPosition, LinkedList<Position>>();
	}

	public void addApplicationListener(ApplicationListener appListener)
	{
		this.appListeners.add(appListener);
	}

	public void addPositionListener(PositionListener posListener)
	{
		this.posListeners.add(posListener);
	}

	public void fireShowTagPositions(Map<String, List<Position>> tagPositions)
	{
		for (ApplicationListener appListener : appListeners)
		{
			appListener.showTagPositions(tagPositions);
		}
	}

	public Color getColorForTagId(String tagId)
	{
		return tagIdColors.get(tagId);
	}

	public Color getColorForTagPosition(TagPosition tagPosition)
	{
		if (TagPosition.REFERENCE == tagPosition)
			return Color.YELLOW;

		if (TagPosition.RIGHT == tagPosition)
			return Color.GREEN;

		if (TagPosition.LEFT == tagPosition)
			return Color.RED;

		if (TagPosition.CHEST == tagPosition)
			return Color.MAGENTA;

		if (TagPosition.BACK == tagPosition)
			return Color.CYAN;

		String tagId = this.tagAssignment.get(tagPosition);
		return getColorForTagId(tagId);
	}

	public File getDefaultOpenMeasurementFolder()
	{
		return saveFolder;
	}

	public String getHost()
	{
		return this.host;
	}

	public Color getNextColor()
	{
		return availableColors[currentColorIndex++ % availableColors.length];
	}

	public int getNumberOfTracingTagIds()
	{
		return this.tracedTagIds.size();
	}

	public int getPort()
	{
		return this.port;
	}

	public List<String> getTracedTagIds()
	{
		return tracedTagIds;
	}

	public boolean isTracingTag(String tagId)
	{
		return this.tracedTagIds.contains(tagId);
	}

	public Measurement loadMeasurement(File measurementFile) throws FileNotFoundException
	{
		return Helper.readMeasurementSeries(new FileInputStream(measurementFile));
	}

	private Map<TagPosition, Position> filteredPositions = new TreeMap<TagPosition, Position>();

	@Override
	public void positionChanged(Position newPosition)
	{
		Position pos = newPosition;

		// add the position as recent tag position
		String tagId = newPosition.getId();
		TagPosition tagPos = reverseTagAssignment.get(tagId);
		if (tagPos != null)
		{
			// add the current position to the recent tag positions
			LinkedList<Position> list = this.recentTagPositions.get(tagPos);
			if (list == null)
			{
				list = new LinkedList<Position>();
				this.recentTagPositions.put(tagPos, list);
			}

			// trim the list if it is larger than the max number of recently
			// saved tags
			list.addFirst(newPosition);
			if (list.size() > MAX_NUMBER_OF_RECENT_TAGS)
				list.removeLast();

			// apply outlier detection and filter
			if (list.size() == MAX_NUMBER_OF_RECENT_TAGS)
			{
				pos = calculatePos(list);
				filteredPositions.put(tagPos, pos);
			}

			// inform all listeners that the position of given tag has changed
			for (TagPositionListener tagPositionListener : tagPositionListeners)
			{
				tagPositionListener.tagPositionChanged(tagPos, pos);
			}

			// gesture recognition
			if (filteredPositions.size() >= 4)
			{
				Position leftPos = filteredPositions.get(TagPosition.LEFT);
				Position rightPos = filteredPositions.get(TagPosition.RIGHT);
				Position refPos = calculateReferencePosition();
				filteredPositions.put(TagPosition.REFERENCE, refPos);

				// inform listeners for updated reference position
				for (TagPositionListener tagPositionListener : tagPositionListeners)
				{
					tagPositionListener.tagPositionChanged(TagPosition.REFERENCE, refPos);
				}

				GestureCharacteristics gc = new GestureCharacteristics(leftPos, refPos, rightPos);
				if (this.rmp != null)
					this.rmp.setGestureCharacteristics(gc);

				// recognize gesture
				Gesture recognizedGesture = gestureRecognizer.getRecognizedGesture(leftPos, refPos, rightPos);
				addGesture(recognizedGesture);
			}
		}

		for (PositionListener positionListener : posListeners)
		{
			positionListener.positionChanged(newPosition);
		}
	}

	private void addGesture(Gesture recognizedGesture)
	{
		recentGestures.addFirst(new GestureObject(recognizedGesture));

		boolean didRemoveOldest = false;

		// delete gestures which are older than the gesture recognition interval
		Iterator<GestureObject> descendingIterator = recentGestures.descendingIterator();
		while (descendingIterator.hasNext())
		{
			GestureObject gestureObject = descendingIterator.next();
			if (Helper.isOlderThan(gestureObject.getTimestamp(), gestureRecognitionTime))
			{
				descendingIterator.remove();
				didRemoveOldest = true;
			}
		}

		if (!didRemoveOldest)
			return;

		// calculate occurences
		Bag histogram = new HashBag();
		for (GestureObject gesture : recentGestures)
		{
			histogram.add(gesture.getGesture());
		}

		int max = 0;
		Gesture maxGesture = null;
		for (Object object : histogram)
		{
			Gesture gesture = (Gesture) object;
			int count = histogram.getCount(gesture);
			if (count > max)
			{
				max = count;
				maxGesture = gesture;
			}
		}

		double percent = max / (double) recentGestures.size();
		if (percent >= gestureRecognitionTreshold)
		{
			if (maxGesture != Gesture.NONE && maxGesture != Gesture.LDRD)
				System.out.println(maxGesture + "(" + max + ") / " + recentGestures.size() + " " + percent * 100.0
						+ "%");

			// clear all recent gestures
			recentGestures.clear();

			// inform listeners
			for (GestureListener gestureListener : gestureListeners)
			{
				gestureListener.gestureRecognized(maxGesture, new Date());
			}
		}
	}

	public void setGestureRecognitionTime(long gestureRecognitionTime)
	{
		this.gestureRecognitionTime = gestureRecognitionTime;
	}

	public void setGestureRecognitionTreshold(double gestureRecognitionTreshold)
	{
		this.gestureRecognitionTreshold = gestureRecognitionTreshold;
	}

	public long getGestureRecognitionTime()
	{
		return gestureRecognitionTime;
	}

	public double getGestureRecognitionTreshold()
	{
		return gestureRecognitionTreshold;
	}

	private GestureRecognizer gestureRecognizer = new AngleGestureRecognizer();
	private LinkedList<GestureObject> recentGestures = new LinkedList<GestureObject>();

	private long gestureRecognitionTime = 1000;
	private double gestureRecognitionTreshold = 0.8d;

	private ReplayMeasurementPanel rmp;

	private double zOffset = 0.2d;

	public void setZOffset(double zOffset)
	{
		this.zOffset = zOffset;
		System.out.println("zOffset: " + this.zOffset);
	}

	public void setReplayMeasurementPanel(ReplayMeasurementPanel rmp)
	{
		this.rmp = rmp;
	}

	private Position calculateReferencePosition()
	{
		Position chestPos = filteredPositions.get(TagPosition.CHEST);
		Position backPos = filteredPositions.get(TagPosition.BACK);

		List<Position> list = Arrays.asList(chestPos, backPos);
		LinkedList<Position> linkedList = new LinkedList<Position>(list);

		LinkedList<Position> filteredPositions = new MeanFilter().applyFilter(linkedList);

		Position refPos = filteredPositions.getFirst();
		refPos.setId("reference");
		refPos.setDate(Helper.now());

		// add Z offset
		refPos.setZ(refPos.getZ() + zOffset);

		return refPos;
	}
	private List<GestureListener> gestureListeners = new LinkedList<GestureListener>();

	public void addGestureListener(GestureListener gestureListener)
	{
		gestureListeners.add(gestureListener);
	}

	private Position calculatePos(LinkedList<Position> list)
	{
		Filter meanFilter = new MeanFilter();
		LinkedList<Position> meanApplied = meanFilter.applyFilter(list);

		return meanApplied.getFirst();
	}

	public void removeApplicationListener(ApplicationListener appListener)
	{
		this.appListeners.remove(appListener);
	}

	public void removePositionListener(PositionListener posListener)
	{
		this.posListeners.remove(posListener);
	}

	public void saveMeasurement(Measurement measurement)
	{
		System.out.println("saving measurement \n" + measurement.toString());

		Date startDate = measurement.getStartDate();
		String fileName = dateFormat.format(startDate) + ".xml";
		File measurementFile = new File(saveFolder, fileName);
		Helper.writeMeasurement(measurement, measurementFile);
	}

	public void setColorForTagId(String tagId, Color color)
	{
		tagIdColors.put(tagId, color);
	}

	public void setHost(String host)
	{
		this.host = host;
	}

	public void setPort(int port)
	{
		this.port = port;
	}

	public void startedTracingTag(String id)
	{
		this.tracedTagIds.add(id);
		for (ApplicationListener appListener : appListeners)
		{
			appListener.startedTracingTag(id);
		}
	}

	public void startListening()
	{
		startListening(host, port);
	}

	public void startListening(String host, int port)
	{
		this.listener.startListening(host, port);

		for (ApplicationListener appListener : appListeners)
		{
			appListener.startedListening(host, port);
		}
	}

	public void stopListening()
	{
		this.listener.stopListening();
		for (String tagId : tracedTagIds)
		{
			stoppedTracingTag(tagId);
		}
		tracedTagIds.clear();

		for (ApplicationListener appListener : appListeners)
		{
			appListener.stoppedListening(host, port);
		}
	}

	public void stoppedTracingTag(String id)
	{
		for (ApplicationListener appListener : appListeners)
		{
			appListener.stoppedTracingTag(id);
		}
	}

	public void stopTracingTag(String tagId)
	{
		this.tracedTagIds.remove(tagId);
		stoppedTracingTag(tagId);
	}

	public void tagAssignmentChanged(TagPosition tagPosition, String tagId)
	{
		tagAssignment.put(tagPosition, tagId);
		reverseTagAssignment.put(tagId, tagPosition);

		for (ApplicationListener appListener : appListeners)
		{
			appListener.tagAssignmentChanged(tagPosition, tagId);
		}
	}

	private List<TagPositionListener> tagPositionListeners = new LinkedList<TagPositionListener>();

	private double angleTolerance = GestureCharacteristics.ANGLE_TOLERANCE;
	private double equalToHeightTolerance = GestureCharacteristics.EQUAL_HEIGHT_TOLERANCE;
	private double lowerDifferenceTolerance = GestureCharacteristics.LOWER_TOLERANCE;

	public double getAngleTolerance()
	{
		return angleTolerance;
	}

	public double getEqualToHeightTolerance()
	{
		return equalToHeightTolerance;
	}

	public double getLowerDifferenceTolerance()
	{
		return lowerDifferenceTolerance;
	}

	public void addTagPositionListener(TagPositionListener tagPositionListener)
	{
		this.tagPositionListeners.add(tagPositionListener);
	}

	public void setAngleTolerance(double angleTolerance)
	{
		this.angleTolerance = angleTolerance;
		GestureCharacteristics.ANGLE_TOLERANCE = this.angleTolerance;
	}

	public void setEqualHeightTolerance(double equalToHeightTolerance)
	{
		this.equalToHeightTolerance = equalToHeightTolerance;
		GestureCharacteristics.EQUAL_HEIGHT_TOLERANCE = this.equalToHeightTolerance;
	}

	public void setLowerDifferenceTolerance(double lowerDifferenceTolerance)
	{
		this.lowerDifferenceTolerance = lowerDifferenceTolerance;
		GestureCharacteristics.LOWER_TOLERANCE = this.lowerDifferenceTolerance;
	}

	public double getZOffset()
	{
		return zOffset;
	}
}
